/*
PearReview - The peer review assistant.
Copyright (C) 2009  Dimitrij Pankratz, Anton Musichin
http://www.pearreview.com, info@pearreview.com

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

package com.pearreview.application;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.LinkedList;

import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.JTabbedPane;
import javax.swing.JToolBar;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;

import com.pearreview.ui.CloseButtonTabComponent;
import com.pearreview.ui.MainTabsUI;

public class PearReviewFrame extends JFrame {

	private static final long serialVersionUID = -6193641557752325448L;
	
	private String title;
	
	private EventListenerList viewCloseListenerList;
	private EventListenerList fullScreenListenerList;
	
	private JTabbedPane tabbedPane;
	private ChangeListener changeListener = new TabChangeListener();
	private LinkedList<View> viewList;
	
	private boolean isFullScreen = false;
	private boolean isFullScreenExclusiveModeEnabled = true;
	private boolean isSwitchingFullScreen = false;
	private boolean fullScreenRestoreMaximized = false;
	private Dimension fullScreenRestoreMininmumSize;
	private Rectangle fullScreenRestoreBounds;
	
	
	private Component lastComponent = null;
	private JToolBar lastJToolBar = null;
	private JMenuBar lastJMenuBar = null;
	
	
	/**
	 * Constructs a new frame that is initially invisible. 
	 * This constructor sets the component's locale property to the value 
	 * returned by JComponent.getDefaultLocale. 
	 * @throws HeadlessException if GraphicsEnvironment.isHeadless() 
	 * returns true.
	 */
	public PearReviewFrame() throws HeadlessException {
		super();
		initialize();
	}
	
	/**
	 * Creates a Frame in the specified GraphicsConfiguration of a 
	 * screen device and a blank title. 
	 * This constructor sets the component's locale property to 
	 * the value returned by JComponent.getDefaultLocale. 
	 * @param gc the GraphicsConfiguration that is used to construct 
	 * the new Frame; if gc is null, the system default 
	 * GraphicsConfiguration is assumed 
	 * 
	 * @throws IllegalArgumentException if gc is not from a screen device. 
	 * This exception is always thrown when GraphicsEnvironment.isHeadless() 
	 * returns true.
	 */
	public PearReviewFrame(GraphicsConfiguration gc) {
		super(gc);
		initialize();
	}
	
	/**
	 * Creates a new, initially invisible Frame with the specified title. 
	 * This constructor sets the component's locale property to the value 
	 * returned by JComponent.getDefaultLocale. 
	 * @param title the title for the frame 
	 * @throws HeadlessException if GraphicsEnvironment.isHeadless() 
	 * returns true.
	 */
	public PearReviewFrame(String title) throws HeadlessException {
		super(title);
		this.title = title;
		initialize();
	}
	
	/**
	 * Creates a JFrame with the specified title and the specified 
	 * GraphicsConfiguration of a screen device. 
	 * This constructor sets the component's locale property to 
	 * the value returned by JComponent.getDefaultLocale. 
	 * @param title the title to be displayed in the frame's border. 
	 * A null value is treated as an empty string, "".
	 * @param gc the GraphicsConfiguration that is used to construct the new 
	 * JFrame with; if gc is null, the system default GraphicsConfiguration 
	 * is assumed 
	 * 
	 * @throws IllegalArgumentException if gc is not from a screen device. 
	 * This exception is always thrown when GraphicsEnvironment.isHeadless() 
	 * returns true.
	 */
	public PearReviewFrame(String title, GraphicsConfiguration gc) {
		super(title, gc);
		this.title = title;
		initialize();
	}
	
	public static boolean isMacOSX() {
		return System.getProperty("os.name").toLowerCase()
				.startsWith("mac os x");
	}
	
	@Override
	public void setTitle(String title) {
		this.title = title;
	}
	
	@Override
	public String getTitle() {
		return title;
	}
	
	private void initialize() {
		if(isMacOSX()) {
			System.setProperty("apple.laf.useScreenMenuBar", "true");
		}
		
		viewCloseListenerList = new EventListenerList();
		fullScreenListenerList = new EventListenerList();
		
		viewList = new LinkedList<View>();
		
		tabbedPane = new JTabbedPane();
		tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
		tabbedPane.setUI(new MainTabsUI());
		tabbedPane.addChangeListener(changeListener);
		
		addWindowListener(new FullScreenWindowListener());
	}
	
	protected void setTitle(View v) {
		String newTitle = getTitle();
		
		if(v != null) {
			if(v.getFrameTitle() != null && !v.getFrameTitle().isEmpty()) {
				newTitle += " - " + v.getFrameTitle();
			}
			
			if(v.getViewTitle() != null && !v.getViewTitle().isEmpty()) {
				newTitle += " - " + v.getViewTitle();
			}
			
			if(isMacOSX()) {
				getRootPane().putClientProperty("Window.documentFile", 
						v.getFile());
			}
		}
		
		super.setTitle(newTitle);
	}
	
	private void setView(View v) {
		Container container = getContentPane();
		
		/* title */
		setTitle(v);
		
		if(v == null) {
			container.removeAll();
			validate();
			return;
		}
		
		/* tool bar */
		if(v.getToolBar() != lastJToolBar) {
			if(lastJToolBar != null) {
				container.remove(lastJToolBar);
				lastJToolBar = null;
				validate();
			}
			if(v.getToolBar() != null) {
				lastJToolBar = v.getToolBar();
				container.add(lastJToolBar, BorderLayout.NORTH);
				validate();
			}
		}
		
		/* menu bar */
		if(v.getMenuBar() != lastJMenuBar) {
			lastJMenuBar = v.getMenuBar();
			setJMenuBar(lastJMenuBar);
			validate();
		}
		
		/* component */
		Component comp = sizeView() > 1 ? tabbedPane : v.getComponent();
		if(comp != lastComponent) {
			if(lastComponent != null) {
				container.remove(lastComponent);
				lastComponent = null;
				validate();
			}
			if(comp != null) {
				lastComponent = comp;
				container.add(lastComponent);
				validate();
			}
		}
	}
	
	protected CloseButtonTabComponent createCloseButtonTabComponent(
			final View v) {
		CloseButtonTabComponent tabComponent = 
				new CloseButtonTabComponent(tabbedPane);
		
		tabComponent.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				ViewCloseEvent event = 
						new ViewCloseEvent(this, v);
				fireViewCloseListeners(event);
			}					
		});

		return tabComponent;
	}
	/**
	 * Adds a View to this component
	 * @param v adds this view
	 */
	public void addView(View v) {
		addView(viewList.size(), v);
	}
	
	/**
	 * Adds a View to this component
	 * @param v adds this view
	 * @param index index at which the specified view is to be inserted
	 */
	public void addView(int index, View v) {
		if(v == null) {
			throw new NullPointerException();
		}
		
		viewList.add(index, v);
		
		if(viewList.size() == 2) {
			tabbedPane.removeAll();
			for(int i = 0 ; i < viewList.size() ;i++) {
				View currentView = viewList.get(i);
				tabbedPane.addTab(currentView.getTabTitle(),
						currentView.getIcon(),
						currentView.getComponent());
				
				tabbedPane.setTabComponentAt(i, 
						createCloseButtonTabComponent(currentView));
			}
		} else if(viewList.size() > 2) {
			tabbedPane.addTab(v.getTabTitle(),
					v.getIcon(),
					v.getComponent());
			
			tabbedPane.setTabComponentAt(tabbedPane.getTabCount() - 1, 
					createCloseButtonTabComponent(v));
		}
		
		setView(v);
	}
	
	/**
	 * Returns the view at the specified position. 
	 * @param index index of the view to return 
	 * @return the view at the specified position 
	 */
	public View getView(int index) {
		return viewList.get(index);
	}
	
	/**
	 * Returns all views. 
	 * @return all views, where added by <code>addView</code> 
	 */
	public View[] getViews() {
		View[] views = new View[viewList.size()];
		return viewList.toArray(views);
	}
	
	/**
	 * Returns the selected view
	 * @return selected view
	 */
	public View getSelectedView() {
		final int index = getSelectedViewIndex();
		if(index < 0)
			return null;
		
		return getView(index);
	}
	
	/**
	 * Returns the selected view-index
	 * @return selected view-index
	 */
	public int getSelectedViewIndex() {		
		switch(sizeView()) {
			case 0:		return -1;
			case 1: 	return 0;
			default:	return tabbedPane.getSelectedIndex();
		}		
	}
	
	/**
	 * Removes view at the specified position
	 * @param index index of view to remove
	 * @return the removed view
	 */
	public View removeView(int index) {
		View v = viewList.remove(index);
		
		if(viewList.size() <= 0) {
			setView(null);
		} else if(viewList.size() == 1) {			
			setView(getSelectedView());
		} else {
			tabbedPane.removeTabAt(index);
		}
		
		return v;
	}
	
	/**
	 * Removes all views
	 */
	public void clearViews() {
		viewList.clear();
		setView(null);
	}
	
	public void setView(int index, View v) {
		viewList.set(index, v);
		validateView(v);
	}
	
	/**
	 * Removes view at the specified position
	 * @param view view to remove
	 * @return the index of removed view
	 */
	public int removeView(View v) {
		final int index = indexOfView(v);
		removeView(index);
		return index;
	}
	
	/**
	 * Returns a index of specified view
	 * @param v view to search for 
	 * @return the index of the first occurrence of the specified 
	 * view in this list, or -1 if this list does not contain the view
	 */
	public int indexOfView(View v) {
		return viewList.indexOf(v);
	}
	
	/**
	 * Returns the number of views.
	 * @return the number of views
	 */
	public int sizeView() {
		return viewList.size();
	}
	
	/**
	 * Returns true if this list contains the specified view. 
	 * @param v view whose presence in this list is to be tested 
	 * @return true if this list contains the specified view
	 */
	public boolean containsView(View v) {
		return viewList.contains(v);
	}	
	
	/**
	 * Selects specified view
	 * @param index index of view to select
	 */
	public void setSelectedView(int index) throws IndexOutOfBoundsException {
		if(getSelectedViewIndex() == index)
			return;
		
		if(index < 0 || index >= sizeView())
			throw new IndexOutOfBoundsException();
		
		if(sizeView() > 1) {
			tabbedPane.setSelectedIndex(index);
		}
	}
	
	/**
	 * Selects specified view
	 * @param index view to select
	 */
	public void setSelectedView(View v) {
		final int index = indexOfView(v);
		setSelectedView(index);
	}
	
	/**
	 * Validate view, if necessary the view will be repainted.
	 * @param index index of view to validate
	 */
	public void validateView(int index) {		
		if(index < 0 || index >= sizeView()) {
			throw new IndexOutOfBoundsException();
		}
		
		
		View v = getView(index);
		setView(v);
		if(sizeView() > 1) {
			Component comp = tabbedPane.getComponentAt(index);
			if(comp != v.getComponent()) {
				tabbedPane.setComponentAt(index, v.getComponent());
			}
			tabbedPane.setTitleAt(index, v.getTabTitle());
			tabbedPane.setIconAt(index, v.getIcon());
			tabbedPane.setTabComponentAt(index, 
					createCloseButtonTabComponent(v));
		}
	}
	
	/**
	 * Validate view, if necessary the view will be repainted.
	 * @param v view to validate
	 */
	public void validateView(View v) {		
		final int index = indexOfView(v);
		validateView(index);
	}
	
	/**
	 * Adds the listener. This will be dispatched when 
	 * user click on close-tab-button
	 * @param vcl listener to add
	 */
	public void addViewCloseListener(ViewCloseListener vcl) {
		viewCloseListenerList.add(ViewCloseListener.class, vcl);
	}
	
	/**
	 * Return an array of all the listeners of the given type.
	 * @param vcl listener to remove
	 */
	public void removeViewCloseListener(ViewCloseListener vcl) {
		viewCloseListenerList.remove(ViewCloseListener.class, vcl);
	}
	
	/**
	 * Return an array of all the listeners. 
	 * @return
	 */
	protected ViewCloseListener[] getViewCloseListeners() {
		return viewCloseListenerList.getListeners(ViewCloseListener.class);
	}
	
	protected void fireViewCloseListeners(ViewCloseEvent event) {
		ViewCloseListener[] listeners = getViewCloseListeners();
		for (int i = 0; i < listeners.length ; i++ ) {
			listeners[i].viewClosePerformed(event);
		}
	}
	
	/**
	 * Return the full screen mode of frame.
	 * @return <code>true</code> if frame is in full screen mode,
	 * otherwise <code>false</code>
	 */
	public boolean isFullScreen() {
		return isFullScreen;
	}
	
	public int getGraphicsDevicesCount() {
		GraphicsEnvironment ge = 
				GraphicsEnvironment.getLocalGraphicsEnvironment();
		int counter = 0;
		for(GraphicsDevice gd: ge.getScreenDevices()) {
			if(gd.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
				counter ++;
			}
		}
		return counter;
	}
	
	public GraphicsDevice[] getGraphicsDevices() {
		GraphicsEnvironment ge = 
				GraphicsEnvironment.getLocalGraphicsEnvironment();
		GraphicsDevice[] gds = new GraphicsDevice[getGraphicsDevicesCount()];
		int i = 0;
		for(GraphicsDevice gd: ge.getScreenDevices()) {
			if(gd.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
				gds[i] = gd;
				i++;
			}
		}
		
		return gds;
	}
	
	public GraphicsDevice getLocationGraphicsDevice() {
		GraphicsDevice lgd = GraphicsEnvironment.getLocalGraphicsEnvironment()
				.getDefaultScreenDevice();
		double f = 0.0;
		for(GraphicsDevice gd: getGraphicsDevices()) {
			if(gd == null) {
				continue;
			}
			Rectangle2D gdb = getGraphicsDeviceBounds(gd);
			gdb = gdb.createIntersection(getBounds());
			double gdf = gdb.getWidth() * gdb.getHeight() / 
					(getBounds().getWidth() * getBounds().getHeight());
			if(gdf > f) {
				f = gdf;
				lgd = gd;
			}
		}
		
		return lgd;
	}
	
	public Rectangle getLocationGraphicsDeviceBounds() {
		return getGraphicsDeviceBounds(getLocationGraphicsDevice());
	}
	
	public boolean isInVisibleRange() {
		for(GraphicsDevice gd: getGraphicsDevices()) {
			if(gd == null) {
				continue;
			}
			Rectangle2D gdb = getGraphicsDeviceBounds(gd);
			gdb = gdb.createIntersection(getBounds());
			if(!gdb.isEmpty()) {
				return true;
			}
		}
		return false;
	}
	
	public boolean isMaximized() {
		return (getExtendedState() & JFrame.MAXIMIZED_BOTH) > 0;
	}
	
	public void setMaximized(boolean b) {
		if(isMaximized() && !b) {
			setExtendedState(getExtendedState() ^ JFrame.MAXIMIZED_BOTH);
		} else if(!isMaximized() && b) {
			setExtendedState(getExtendedState() | JFrame.MAXIMIZED_BOTH);
		}
	}
	
	/**
	 * Returns default bounds of graphics device
	 * @param gd
	 * @return bounds of graphics device
	 */
	public Rectangle getGraphicsDeviceBounds(GraphicsDevice gd) {
		return gd.getDefaultConfiguration().getBounds();
	}
	
	/**
	 * Sets the frame full screen mode.
	 * @param fs the full screen mode: 
	 * <code>true</code> enabled, <code>false</code> disabled.
	 */
	public void setFullScreen(boolean fs) {
		isSwitchingFullScreen = true;
		if (isFullScreen && !fs) {
			isFullScreen = false;
			dispose();
			setResizable(true);
			setUndecorated(false);
				

			/* restore frame size and location */
			setMinimumSize(fullScreenRestoreMininmumSize);
			setBounds(fullScreenRestoreBounds);
			setMaximized(fullScreenRestoreMaximized);
			
			/* check if current bounds are in visible range */
			if(!isInVisibleRange()) {
				setMaximized(false);
				setSize(getPreferredSize());
				Rectangle gdb = getLocationGraphicsDeviceBounds();
				Rectangle fb = getBounds();
				int top = (gdb.height - fb.height) / 2;
				int left = (gdb.width - fb.width) / 2;
				setLocation(left, top);
			}
			
			setVisible(true);
			setAlwaysOnTop(false);
			
			/* restore title - why this necessary is unknown */
			setTitle(getSelectedView());
			
			/* fire event */
			fireFullScreenListeners();
		} else if(!isFullScreen && fs) {
			isFullScreen = true;
			boolean isActiveWindow = isActive();
			Rectangle bounds = getLocationGraphicsDeviceBounds();
			
			/* store frame size and location */
			fullScreenRestoreMaximized = isMaximized();
			if(fullScreenRestoreMaximized) {
				setMaximized(false);
			}
			
			fullScreenRestoreBounds = getBounds();
			fullScreenRestoreMininmumSize = getMinimumSize();
			
			
			/* set full screen */
			dispose();
			setResizable(false);
			setUndecorated(true);
			
			setBounds(bounds);
			setMinimumSize(bounds.getSize());
			
			if(isActiveWindow) {
				setFullScreenExclusiveMode(true);
			}
			
			setVisible(true);
			setAlwaysOnTop(true);
			
			/* fire event */
			fireFullScreenListeners();
		}
		isSwitchingFullScreen = false;
	}
	
	/**
	 * Enables or disables the full screen exclusive mode
	 * @param b <code>true</code> to enable, <code>false</code> to disable
	 */
	public void setFullScreenExclusiveModeEnabled(boolean b) {
		if(isFullScreen() && b != isFullScreenExclusiveModeEnabled()) {
			setFullScreenExclusiveMode(b);
		}
		isFullScreenExclusiveModeEnabled = b;
		
	}
	
	/**
	 * Checks if frame use full screen exclusive mode
	 * @return <code>true</code> is enabled, otherwise <code>false</code>
	 */
	public boolean isFullScreenExclusiveModeEnabled() {
		return isFullScreenExclusiveModeEnabled;
	}
	
	private void setFullScreenExclusiveMode(boolean fs) {
		setFullScreenExclusiveMode(getLocationGraphicsDevice(), fs);
	}
	
	private void setFullScreenExclusiveMode(GraphicsDevice gd, boolean fs) {
		if(isFullScreenExclusiveModeEnabled()) {
			gd.setFullScreenWindow(fs ? this : null);
		}
	}
	
	/**
	 * Adds a full screen change listener
	 * @param cl listener to add
	 */
	public void addFullScreenListener(ChangeListener cl) {
		fullScreenListenerList.add(ChangeListener.class, cl);
	}
	
	/**
	 * Removes a full screen change listener
	 * @param cl listener to remove
	 */
	public void removeFullScreenListener(ChangeListener cl) {
		fullScreenListenerList.remove(ChangeListener.class, cl);
	}
	
	/**
	 * Return an array of all the listeners. 
	 * @return
	 */
	protected ChangeListener[] getFullScreenListeners() {
		return fullScreenListenerList.getListeners(ChangeListener.class);
	}
	
	protected void fireFullScreenListeners() {
		ChangeEvent e = new ChangeEvent(this);
		fireFullScreenListeners(e);
	}
	
	protected void fireFullScreenListeners(ChangeEvent e) {
		ChangeListener[] listeners = getFullScreenListeners();
		for (int i = 0; i < listeners.length ; i++ ) {
			listeners[i].stateChanged(e);
		}
	}
	
	/**
	 * Gets called when tab index has been changed by user
	 *
	 */
	private class TabChangeListener implements ChangeListener, Serializable {
		private static final long serialVersionUID = -2453974551240113410L;

		@Override
		public void stateChanged(ChangeEvent e) {
			if(getSelectedViewIndex() < 0 || sizeView() <= 1) {
				return;
			}
			
			setView(getSelectedView());
		}
	}
	
	private class FullScreenWindowListener extends WindowAdapter
			implements Serializable {
		private static final long serialVersionUID = -1333153274196907903L;
		@Override
		public void windowActivated(WindowEvent e) {
			if(isFullScreen && !isSwitchingFullScreen) {
				setFullScreenExclusiveMode(true);
			}
		}
		@Override
		public void windowDeactivated(WindowEvent e) {
			if(isFullScreen && !isSwitchingFullScreen) {
				setFullScreenExclusiveMode(false);
			}
		}
	}
}
